/*
 * Copyright 2013 Sigurd Randoll <srandoll@digiway.de>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.digiway.rapidbreeze.server.model.download;

import de.digiway.rapidbreeze.server.model.download.action.DownloadManagerAction;
import de.digiway.rapidbreeze.server.model.download.action.DownloadManagerActionExecutor;
import de.digiway.rapidbreeze.server.config.ServerConfiguration;
import de.digiway.rapidbreeze.server.model.storage.StorageProvider;
import de.digiway.rapidbreeze.server.model.storage.StorageProviderRepository;
import de.digiway.rapidbreeze.server.model.storage.FileStatus;
import de.digiway.rapidbreeze.server.model.storage.StorageProviderDownloadClient;
import de.digiway.rapidbreeze.server.model.storage.UrlStatus;
import de.digiway.rapidbreeze.shared.rest.download.DownloadStatus;
import static de.digiway.rapidbreeze.shared.rest.download.DownloadStatus.PAUSE;
import static de.digiway.rapidbreeze.shared.rest.download.DownloadStatus.RUNNING;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.Validate;

/**
 * An instance of the the {@linkplain DownloadManager} handles
 * {@linkplain Download} instances. To avoid threading issues and to accomplish
 * a synchronous execution of actions related to the
 * {@linkplain DownloadManager}, all operations are queued up and executed in
 * the order of arrival.
 *
 * @author Sigurd Randoll <srandoll@digiway.de>
 */
public class DownloadManager {

    private Map<String, Download> downloadCacheIdentifiers;
    private List<Download> downloadCache;
    private DownloadRepository downloadRepository;
    private StorageProviderRepository storageProviderRepository;
    private Path downloadTargetFolder;
    private ServerConfiguration applicationConfiguration;
    private DownloadListenerHandler listenerHandler;
    private List<DownloadManagerListener> listeners;
    private ThrottleSupport throttleSupport;
    private DownloadManagerActionExecutor actionExecutor;
    private Executor threadExecutor = Executors.newSingleThreadExecutor();
    private static final Logger LOG = Logger.getLogger(DownloadManager.class.getName());

    public DownloadManager(DownloadRepository downloadRepository, StorageProviderRepository storageProviderRepository, ServerConfiguration applicationConfiguration) {
        this.downloadRepository = downloadRepository;
        this.storageProviderRepository = storageProviderRepository;
        this.applicationConfiguration = applicationConfiguration;
        this.downloadTargetFolder = applicationConfiguration.getDownloadTargetFolder();
        this.listeners = new CopyOnWriteArrayList<>();
        this.downloadCacheIdentifiers = new HashMap<>();
        this.downloadCache = new ArrayList<>();
        this.listenerHandler = new DownloadListenerHandler();
        this.actionExecutor = new DownloadManagerActionExecutor();

        this.actionExecutor.start();
        this.throttleSupport = new ThrottleSupport(this);

        for (Download download : downloadRepository.getDownloads()) {
            downloadCacheIdentifiers.put(download.getIdentifier(), download);
            downloadCache.add(download);
            download.addListener(listenerHandler);
        }
        checkTargetFolder();
        threadExecutor.execute(new DownloadHandlingThread());
    }

    /**
     * Throttles the overall download speed to the given bytes per second.
     *
     * @param bytesPerSecond
     */
    public void setThrottle(long bytesPerSecond) {
        executeAtomic(new SetThrottleAction(bytesPerSecond));
    }

    /**
     * Checks if this {@linkplain DownloadManager} can handle the given
     * {@linkplain URL}.
     *
     * @param url
     * @return true if yes
     */
    public boolean canHandleUrl(URL url) {
        return storageProviderRepository.getStorageProvider(url) != null;
    }

    /**
     * Returns an unmodifiable list of all available {@linkplain Download}
     * instances.
     *
     * @return
     */
    public List<Download> getDownloads() {
        return executeAtomic(new GetDownloadsAction());
    }

    /**
     * Checks if the {@linkplain DownloadManager} instance contains an
     * {@linkplain Download} with the given identifier.
     *
     * @param identifier
     * @return
     */
    public boolean hasDownload(String identifier) {
        Validate.notEmpty(identifier);
        return executeAtomic(new HasDownloadAction(identifier));
    }

    /**
     * Returns the {@linkplain Download} of the given identifier.
     *
     * @param identifier
     * @return
     * @throws IllegalArgumentException if the {@linkplain DownloadManager} does
     * not contain a {@linkplain Download} with the given identifier.
     */
    public Download getDownload(String identifier) {
        Validate.notEmpty(identifier);
        return executeAtomic(new GetDownloadAction(identifier));
    }

    /**
     * Creates a new {@linkplain Download} which will handle the given
     * {@linkplain URL}.
     *
     * @param url
     * @return
     */
    public Download addUrl(URL url) {
        Validate.notNull(url);
        return executeAtomic(new AddUrlAction(url));
    }

    /**
     * Removes the given {@linkplain Download} instance. If the download is
     * currently active, it will be stopped (wait state) and afterwards removed.
     *
     * @param download
     */
    public void removeDownload(Download download) {
        Validate.notNull(download);
        executeAtomic(new RemoveDownloadAction(download));
    }

    /**
     * Starts the download. This will trigger the start of a download
     * asynchronous. The call will returned immediatly. A download can be
     * started and resumed after pausing.
     *
     * @throws IllegalStateException if a download is already active.
     */
    public void startDownload(Download download) {
        Validate.notNull(download);
        executeAtomic(new StartDownloadAction(download));
    }

    /**
     * Pauses the given {@linkplain Download}.
     *
     * @param download
     */
    public void pauseDownload(Download download) {
        Validate.notNull(download);
        executeAtomic(new PauseDownloadAction(download));
    }

    /**
     * Tries to set the given {@linkplain Download} into wait state. Wait state
     * is different do pause in so far that any download in wait state will
     * continue soon (maybe automatically).
     *
     * @param download
     */
    public void waitDownload(Download download) {
        Validate.notNull(download);
        executeAtomic(new WaitDownloadAction(download));
    }

    /**
     * Adds a new {@linkplain DownloadManagerListener} to this
     * {@linkplain DownloadManager}. It will be informed about new
     * {@linkplain Download} instances added to the manager and about status
     * changes of all {@linkplain Download} instances.
     *
     * @param listener
     */
    public void addListener(DownloadManagerListener listener) {
        if (!listeners.contains(listener)) {
            listeners.add(listener);
        }
    }

    /**
     * Removes the listener again.
     *
     * @param listener
     */
    public void removeListener(DownloadManagerListener listener) {
        listeners.remove(listener);
    }

    /**
     * Executes the given {@linkplain DownloadManagerAction} synchronously as an
     * atomic operation.
     *
     * @param <V>
     * @param action
     * @return result of the action
     */
    public <V> V executeAtomic(DownloadManagerAction<V> action) {
        return handleFutureException(actionExecutor.executeAction(action));
    }

    private void checkTargetFolder() {
        if (!Files.exists(downloadTargetFolder)) {
            try {
                Files.createDirectory(downloadTargetFolder);
            } catch (IOException ex) {
                throw new IllegalStateException("Cannot create download directory: " + downloadTargetFolder, ex);
            }
        }
    }

    private <T> T handleFutureException(Future<T> future) {
        try {
            return future.get();
        } catch (InterruptedException ex) {
            throw new IllegalStateException(DownloadManager.class.getSimpleName() + " action was interrupted.", ex);
        } catch (ExecutionException ex) {
            throw new IllegalStateException("Exception during execution of " + DownloadManager.class.getSimpleName() + " action.", ex.getCause());
        }
    }

    private void fireAddEvent(Download download) {
        for (DownloadManagerListener listener : listeners) {
            listener.onDownloadAdded(download);
        }
    }

    private void fireRemoveEvent(Download download) {
        for (DownloadManagerListener listener : listeners) {
            listener.onDownloadRemoved(download);
        }
    }

    private class DownloadListenerHandler implements DownloadListener {

        @Override
        public void onDownloadStatusChange(DownloadEvent event) {
            handleFutureException(actionExecutor.executeAction(new OnDownloadStatusChange(event)));
        }
    }

    private class OnDownloadStatusChange implements DownloadManagerAction {

        private DownloadEvent event;

        public OnDownloadStatusChange(DownloadEvent event) {
            this.event = event;
        }

        @Override
        public Void call() throws Exception {
            try {
                if (DownloadStatus.FINISHED_SUCCESSFUL.equals(event.getNewStatus())) {
                    Download downloadSource = (Download) event.getSource();
                    downloadRepository.update(downloadSource);
                }
            } finally {
                // Always refire event:
                for (DownloadManagerListener listener : listeners) {
                    listener.onDownloadStatusChange(event);
                }
            }
            return null;
        }
    }

    private class SetThrottleAction implements DownloadManagerAction {

        private long speed;

        public SetThrottleAction(long speed) {
            this.speed = speed;
        }

        @Override
        public Void call() throws Exception {
            throttleSupport.setThrottleBytesPerSecond(speed);
            return null;
        }
    }

    private class GetDownloadsAction implements DownloadManagerAction<List<Download>> {

        @Override
        public List<Download> call() throws Exception {
            return Collections.unmodifiableList(downloadCache);
        }
    }

    private class HasDownloadAction implements DownloadManagerAction<Boolean> {

        private String identifier;

        public HasDownloadAction(String identifier) {
            this.identifier = identifier;
        }

        @Override
        public Boolean call() throws Exception {
            return downloadCacheIdentifiers.containsKey(identifier);
        }
    }

    private class GetDownloadAction implements DownloadManagerAction<Download> {

        private String identifier;

        public GetDownloadAction(String identifier) {
            this.identifier = identifier;
        }

        @Override
        public Download call() throws Exception {
            if (!hasDownload(identifier)) {
                throw new IllegalArgumentException("Cannot find " + Download.class.getSimpleName() + " with identifier:" + identifier);
            }
            return downloadCacheIdentifiers.get(identifier);
        }
    }

    private class AddUrlAction implements DownloadManagerAction<Download> {

        private URL url;

        public AddUrlAction(URL url) {
            this.url = url;
        }

        @Override
        public Download call() throws Exception {
            StorageProvider storageProvider = storageProviderRepository.getStorageProvider(url);
            if (storageProvider == null) {
                throw new IllegalArgumentException("The given URL " + url + " cannot be handled. There is no " + StorageProvider.class.getSimpleName() + " registered.");
            }

            StorageProviderDownloadClient downloadClient = storageProvider.createDownloadClient();
            UrlStatus urlStatus = downloadClient.getUrlStatus(url);
            String filename = url.getPath();
            if (urlStatus.getFileStatus().equals(FileStatus.OK)) {
                filename = urlStatus.getFilename();
            }

            Path target = Paths.get(downloadTargetFolder.toString(), filename);
            Download download = new Download(url, target, storageProvider);
            downloadRepository.add(download);
            downloadCacheIdentifiers.put(download.getIdentifier(), download);
            downloadCache.add(download);
            download.addListener(listenerHandler);
            fireAddEvent(download);
            return download;
        }
    }

    private class RemoveDownloadAction implements DownloadManagerAction<Void> {

        private Download download;

        public RemoveDownloadAction(Download download) {
            this.download = download;
        }

        @Override
        public Void call() throws Exception {
            downloadRepository.remove(download);
            downloadCacheIdentifiers.remove(download.getIdentifier());
            downloadCache.remove(download);
            download.removeListener(listenerHandler);
            fireRemoveEvent(download);

            // Get status again. Event listeners might have modified status:
            switch (download.getDownloadStatus()) {
                case PAUSE:
                case RUNNING:
                    download.waitState();
                    download.removeTempFile();
            }
            return null;
        }
    }

    private static class StartDownloadAction implements DownloadManagerAction<Void> {

        private Download download;

        public StartDownloadAction(Download download) {
            this.download = download;
        }

        @Override
        public Void call() throws Exception {
            download.start();
            return null;
        }
    }

    private static class PauseDownloadAction implements DownloadManagerAction<Void> {

        private Download download;

        public PauseDownloadAction(Download download) {
            this.download = download;
        }

        @Override
        public Void call() throws Exception {
            download.pause();
            return null;
        }
    }

    private static class WaitDownloadAction implements DownloadManagerAction<Void> {

        private Download download;

        public WaitDownloadAction(Download download) {
            this.download = download;
        }

        @Override
        public Void call() throws Exception {
            download.waitState();
            return null;
        }
    }

    private class DownloadHandlingThread implements Runnable {

        private boolean running = true;

        @Override
        public void run() {
            while (running) {
                try {
                    Integer idle = executeAtomic(new DownloadHandlingAction());
                    Thread.sleep(idle < 0 ? 0 : idle);
                } catch (RuntimeException ex) {
                    LOG.log(Level.SEVERE, "Exception in " + Download.class.getSimpleName() + " handling class.", ex);
                } catch (InterruptedException ex) {
                    LOG.log(Level.SEVERE, Download.class.getSimpleName() + " interrupted.", ex);
                    running = false;
                }
            }
        }
    }

    private class DownloadHandlingAction implements DownloadManagerAction<Integer> {

        @Override
        public Integer call() throws Exception {
            int idle = 500;
            for (Download download : getDownloads()) {
                Integer nextIdle = download.handle();
                if (nextIdle < idle) {
                    idle = nextIdle;
                }
            }
            return idle;
        }
    }
}
